Skip to content

Reason native#1

Open
brianwk wants to merge 12 commits intomainfrom
reason-native
Open

Reason native#1
brianwk wants to merge 12 commits intomainfrom
reason-native

Conversation

@brianwk
Copy link
Copy Markdown
Contributor

@brianwk brianwk commented Mar 25, 2026

@coderabbitai review

Summary by CodeRabbit

  • New Features

    • Added native server-side rendering support with platform-specific implementations for both Melange/JavaScript and native targets.
    • Enhanced component API with improved type safety for date selection modes.
  • Documentation

    • Expanded README with detailed installation instructions and usage examples.
    • Added example applications demonstrating rendering across multiple platforms.
  • Tests

    • Added output validation and fuzz testing infrastructure to ensure consistency across rendering targets.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the day-picker library to support both JavaScript and native platforms via conditional compilation. It introduces a platform-aware component that delegates to platform-specific implementations, adds a complete native day-picker renderer, updates build configuration to use server-reason-react, expands documentation, and establishes example rendering and parity-testing infrastructure.

Changes

Cohort / File(s) Summary
Core Library Platform Abstraction
src/ReactDayPicker.re
Replaced monolithic JS binding with platform-aware component using switch%platform. Added public types for mode, captionLayout, navLayout, selected, onSelect, and reactNode. Introduced ClientImpl (JS-side conversion via Obj.magic) and delegates to ServerImpl on native.
Native Day-Picker Implementation
src/ReactDayPickerNative.re
Added complete 1,400+ line from-scratch native day-picker UI with date utilities (ISO strings, month grids, week calculations), caption/nav variants, selection logic, focus targeting, and grid rendering with ARIA attributes. Exported factory make with optional props for mode, layout, navigation, animation, and display toggles.
Build Configuration
dune, native/dune, dune-project, reason-react-day-picker.opam, reason-react-day-picker.opam.template
Updated PPX pipeline to include server-reason-react.browser_ppx, added external dependency pin for server-reason-react, removed ppxlib, added reason-react and server-reason-react as dependencies, created native library stanza exposing ReactDayPickerNative module.
Documentation
README.md, example/README.md, .gitignore
Expanded README with installation (Melange/Native/SSR), revised usage examples using ReactDayPicker with typed props, added prop/type tables, CSS reference, HTML structure, and license sections. Added example/README.md describing fixture layout and parity testing workflow. Ignored node_modules.
Example Renderers
example/native/NativeRenderer.re, example/native/RangeNativeRenderer.re, example/js/JsRenderer.re, example/js/RangeJsRenderer.re
Added four simple entry points rendering day-picker configurations to HTML strings via ReactDOM.renderToString (native) or ReactDOMServer.renderToString (JS), with RENDER_START/RENDER_END markers for output comparison.
Example Shared Fixtures
example/shared/ExampleDayPickers.re, example/shared/RangeRenderer.re, example/shared/Scenario.re, example/shared/RangeScenario.re, example/shared/SharedFixture.re
Added scenario configuration modules defining caption/nav layouts, helper functions to resolve scenario names and dates, and fixture renderers for single/range day-picker instances. Exports root React elements and current() scenario selectors parameterized by CLI arguments.
Parity & Fuzz Testing
example/check-parity.sh, example/check-range-parity.sh, example/fuzz-test.sh, example/fuzz/dune, example/fuzz/FuzzGenerator.re, example/fuzz/FuzzRenderer.re, example/fuzz/FuzzTestRunner.re
Added shell scripts to validate rendering parity between native and JS via unified diff and executable fuzz test suite. Fuzz modules generate random configurations, render them via native FuzzRenderer, and execute Alcotest test cases asserting successful normalization.
Build Configuration
example/native/dune, example/js/dune
Defined native and Melange/JS executable targets with file copying from shared fixtures, library dependencies on reason-react-day-picker, preprocessor configuration, and module entry points for both platform renderers.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant RDP as ReactDayPicker<br/>(Wrapper)
    participant PlatformCheck as Platform Detection
    participant ClientImpl as ClientImpl<br/>(JS Binding)
    participant ServerImpl as ServerImpl<br/>(Native)
    participant JSLib as react-day-picker<br/>(JS Library)
    participant NativeRenderer as ReactDayPickerNative<br/>(Native Impl)
    
    App->>RDP: Render with typed props<br/>(selected, onSelect, etc.)
    RDP->>PlatformCheck: switch%platform (Runtime.platform)
    
    alt JS Platform
        PlatformCheck->>ClientImpl: Delegate to JS path
        ClientImpl->>ClientImpl: Convert variants to<br/>Obj.magic raw values
        ClientImpl->>ClientImpl: Map selected/onSelect<br/>via toJsRangeDate
        ClientImpl->>JSLib: React.createElement<br/>with converted props
        JSLib-->>ClientImpl: Returns JS element
        ClientImpl-->>RDP: Returns element
    else Native Platform
        PlatformCheck->>ServerImpl: Delegate to native path
        ServerImpl->>ServerImpl: Convert variants to<br/>ReactDayPickerNative types
        ServerImpl->>ServerImpl: Map dates via<br/>toNativeRangeDate
        ServerImpl->>NativeRenderer: Render with<br/>converted props
        NativeRenderer->>NativeRenderer: Compute month grids,<br/>day cells, ARIA attrs
        NativeRenderer-->>ServerImpl: Returns React element
        ServerImpl-->>RDP: Returns element
    end
    
    RDP-->>App: Rendered component
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A day picker splits in two,
Native and browser paths both new,
With grids of cells and dates arranged,
From JS to servers—platforms changed!
Now single, multiple, ranges bloom,
In every render room. 🌙

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "Reason native" is vague and does not clearly convey the primary change—introducing a universal ReactDayPicker component for both Melange and native platforms. Revise the title to be more specific and descriptive of the main work, such as "Add universal ReactDayPicker component for Melange and native" or "Support native rendering with shared ReactDayPicker API".
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch reason-native

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (12)
reason-react-day-picker.opam.template (1)

1-3: Consider pinning to a specific commit or tag for reproducibility.

Pinning server-reason-react.dev to the repository HEAD without a specific commit hash or tag means builds may produce different results over time as the upstream repo changes. If this is intentional during active development, consider documenting that this should be pinned to a release tag before publishing a stable version.

♻️ Example with commit pinning
 pin-depends: [
-  [ "server-reason-react.dev" "git+https://github.com/ml-in-barcelona/server-reason-react.git" ]
+  [ "server-reason-react.dev" "git+https://github.com/ml-in-barcelona/server-reason-react.git#<commit-sha>" ]
 ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@reason-react-day-picker.opam.template` around lines 1 - 3, The pin-depends
entry currently references "server-reason-react.dev" at repository HEAD; change
the pin-depends value to reference a specific commit hash or release tag (e.g.,
replace the git+https URL with git+https://...#<commit-hash-or-tag>) to ensure
reproducible builds, and if leaving it unpinned during active development is
intentional, add a short comment in the template noting it must be pinned to a
release tag before publishing; update the pin-depends array and the
"server-reason-react.dev" entry accordingly.
ReactDayPickerNative.re (1)

332-337: Week number calculation may differ from ISO 8601.

The getWeekNumber implementation uses a simplified algorithm based on the first day of the year. ISO 8601 week numbering has specific rules (week 1 contains the first Thursday). If strict ISO compliance is needed, this may require adjustment. For most calendar UIs, this approximation is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ReactDayPickerNative.re` around lines 332 - 337, The getWeekNumber function
currently computes weeks by dividing days since year start using dateYear,
newDate, dayOfWeek and getDayOfYear, which does not follow ISO 8601 (week 1
contains the year's first Thursday); to fix, replace or augment getWeekNumber to
implement ISO rules: determine the Thursday-based week anchor (adjust the date
by (4 - dayOfWeek) days), compute the year of that anchor, then compute week
number as 1 + floor((dayOfYear(anchor) - 1) / 7); update getWeekNumber to use
this algorithm (keeping helpers dateYear, newDate, dayOfWeek, getDayOfYear) so
week numbers match ISO 8601 when required.
ReactDayPicker.re (1)

93-106: Obj.magic usage is appropriate for JS interop but inherently unsafe.

The use of Obj.magic to convert polymorphic variants to the raw values expected by react-day-picker is a common pattern in Melange FFI. However, this bypasses type checking:

  • If react-day-picker's API changes, these conversions will silently produce incorrect values
  • Consider adding inline comments documenting the expected JS shapes

This is acceptable for library bindings where the alternative would be much more verbose.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ReactDayPicker.re` around lines 93 - 106, The Obj.magic conversions in
toJsSelected and toJsOnSelect (and the use of toJsRangeDate/fromJsRangeDate) are
unsafe for JS interop; add concise inline comments above toJsSelected and
toJsOnSelect documenting the exact JS shapes and callback signatures expected by
react-day-picker for `Single`, `Multiple`, and `Range` (e.g., Single -> Date,
Multiple -> Array<Date>, Range -> { from?: Date, to?: Date }, and the Range
callback receives that shape), and note that Obj.magic is used intentionally for
brevity so future API changes are easier to spot; keep the comments next to the
function definitions for quick discovery.
example/shared/ExampleDayPickers.re (1)

3-12: Unused makeDate helper function.

This helper function is defined but never called in this file. The date construction on lines 14-24 uses inline Js.Date.make calls instead.

🧹 Either remove or use the helper

Option 1 - Remove unused code:

-let makeDate = (~year: int, ~month: int, ~day: int): Js.Date.t =>
-  Js.Date.make(
-    ~year=float_of_int(year),
-    ~month=float_of_int(month),
-    ~date=float_of_int(day),
-    ~hours=12.0,
-    ~minutes=0.0,
-    ~seconds=0.0,
-    (),
-  );
-

Option 2 - Use the helper for closeDate:

-let closeDate = Js.Date.make(
-  ~year=Js.Date.getFullYear(today),
-  ~month=Js.Date.getMonth(today),
-  ~date=Js.Date.getDate(today) +. 4.0,
-  ~hours=12.0,
-  ~minutes=0.0,
-  ~seconds=0.0,
-  (),
-);
+let closeDate = makeDate(
+  ~year=int_of_float(Js.Date.getFullYear(today)),
+  ~month=int_of_float(Js.Date.getMonth(today)),
+  ~day=int_of_float(Js.Date.getDate(today)) + 4,
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/shared/ExampleDayPickers.re` around lines 3 - 12, The function
makeDate is defined but unused; either remove it or replace the inline
Js.Date.make call used for closeDate with the helper. Locate makeDate and the
closeDate construction in ExampleDayPickers.re, then either delete the makeDate
declaration or refactor closeDate to call makeDate(~year, ~month, ~day) (keep
the existing hours/minutes/seconds behavior by ensuring makeDate produces noon)
so there’s no unused helper left behind.
example/fuzz/FuzzRenderer.re (2)

54-68: Partial range handling differs from RangeRenderer.re.

This logic returns Js.Undefined.fromOption(None) when only from is present (without to), or when only to would be present (not handled). In contrast, RangeRenderer.re (lines 12-24) constructs a valid range object with one bound set and the other undefined.

If partial range selection should be testable via fuzz, consider aligning the logic:

♻️ Handle partial ranges consistently
     | "range" =>
       let range =
         switch (config.rangeFrom) {
         | Some(from) =>
           switch (config.rangeTo) {
           | Some(to_) =>
             Js.Undefined.return({
               ReactDayPicker.from: Js.Undefined.return(from),
               ReactDayPicker.to_: Js.Undefined.return(to_),
-            });
-          | None => Js.Undefined.fromOption(None)
+            })
+          | None =>
+            Js.Undefined.return({
+              ReactDayPicker.from: Js.Undefined.return(from),
+              ReactDayPicker.to_: Js.Undefined.fromOption(None),
+            })
           };
-        | None => Js.Undefined.fromOption(None)
+        | None =>
+          switch (config.rangeTo) {
+          | Some(to_) =>
+            Js.Undefined.return({
+              ReactDayPicker.from: Js.Undefined.fromOption(None),
+              ReactDayPicker.to_: Js.Undefined.return(to_),
+            })
+          | None => Js.Undefined.fromOption(None)
+          }
         };
       `Range(range);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/fuzz/FuzzRenderer.re` around lines 54 - 68, The "range" branch in
FuzzRenderer.re currently returns Js.Undefined.fromOption(None) when only one
bound is present, which prevents creating partial ranges; update the logic to
mirror RangeRenderer.re by constructing a range object even when only from or
only to is present: when config.rangeFrom is Some(from) and config.rangeTo is
None, return a Range where ReactDayPicker.from is Js.Undefined.return(from) and
ReactDayPicker.to_ is Js.Undefined.fromOption(None); likewise when rangeTo is
Some(to_) and rangeFrom is None construct a Range with ReactDayPicker.to_ set
and ReactDayPicker.from as Js.Undefined.fromOption(None); keep the existing
full-range case unchanged so partial selections are testable by the fuzzer.

19-31: Redundant identity transformations can be simplified.

The switch statements for captionLayout and navLayout are identity transformations that map each variant to itself. Since config.captionLayout is already of type option(Scenario.captionLayout), this can be simplified.

♻️ Suggested simplification
-  let captionLayout = switch (config.captionLayout) {
-    | Some(`Label) => Some(`Label)
-    | Some(`Dropdown) => Some(`Dropdown)
-    | Some(`DropdownMonths) => Some(`DropdownMonths)
-    | Some(`DropdownYears) => Some(`DropdownYears)
-    | None => None
-  };
-  let navLayout = switch (config.navLayout) {
-    | Some(`Around) => Some(`Around)
-    | Some(`After) => Some(`After)
-    | None => None
-  };
+  let captionLayout = config.captionLayout;
+  let navLayout = config.navLayout;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/fuzz/FuzzRenderer.re` around lines 19 - 31, The switch blocks in
renderFuzzConfig (for captionLayout and navLayout) are identity mappings;
replace the switch logic by directly using the existing option values: assign
captionLayout = config.captionLayout and navLayout = config.navLayout (i.e.,
remove the redundant switch over config.captionLayout and config.navLayout and
use the fields as-is) so the function is simpler and equivalent.
example/shared/RangeScenario.re (3)

8-17: Consider extracting makeDate to a shared utility module.

This helper is duplicated verbatim in FuzzTestRunner.re (lines 25-34). Extracting it to a shared module (e.g., DateUtils.re) would improve maintainability and reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/shared/RangeScenario.re` around lines 8 - 17, The helper function
makeDate is duplicated and should be extracted into a shared utility module;
create a new module (e.g., DateUtils.re) that exports makeDate with the same
signature (let makeDate = (~year:int, ~month:int, ~day:int): Js.Date.t => ...),
replace the inline implementations in RangeScenario.re and FuzzTestRunner.re
with imports/uses of DateUtils.makeDate, and update any module references or
build entries so both modules use the single shared implementation.

19-21: Unused variable nextWeek.

The nextWeek constant is defined but never used in this module. Consider removing it to avoid dead code, or use it in the scenario definitions if there's a "range-week-long" scenario planned.

🧹 Proposed removal
 let today = makeDate(~year=2026, ~month=2, ~day=21);
 let tomorrow = makeDate(~year=2026, ~month=2, ~day=22);
-let nextWeek = makeDate(~year=2026, ~month=2, ~day=28);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/shared/RangeScenario.re` around lines 19 - 21, The constant
`nextWeek` is declared via `makeDate(~year=2026, ~month=2, ~day=28)` but never
used; either remove the `nextWeek` declaration to eliminate dead code or
integrate `nextWeek` into your scenario definitions (e.g., a week-long range
scenario) where `today` and `tomorrow` are used; update any scenario functions
that should reference a week range to use `nextWeek` (or delete the `nextWeek`
binding if no such scenario is needed).

30-40: Fallback case in byName silently uses default values.

The wildcard pattern at line 37 returns (Some(today), Some(today)) for unrecognized names without warning. Since current() already validates against all, this is fine for internal use, but the function is public. Consider documenting this behavior or making the fallback explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/shared/RangeScenario.re` around lines 30 - 40, The public function
byName currently silently maps unknown names to (Some(today), Some(tomorrow))
via the wildcard branch; update byName to make this behavior explicit by either
(A) replacing the wildcard branch with an explicit failure (e.g., raise/invalid
argument including the unknown name) so callers get immediate feedback, or (B)
keep the fallback but add a clear docstring/comment on byName describing that
unknown names will map to (Some(today), Some(tomorrow)) and that current()/all
validation is expected elsewhere; reference the byName function and the
today/tomorrow identifiers when making the change.
example/fuzz/FuzzTestRunner.re (1)

25-34: Duplicate makeDate helper.

This function is identical to RangeScenario.makeDate. Consider extracting to a shared utility module to avoid duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/fuzz/FuzzTestRunner.re` around lines 25 - 34, The makeDate
implementation in FuzzTestRunner.re duplicates RangeScenario.makeDate; extract
the shared implementation into a common utility (e.g., DateHelpers.makeDate) and
replace the local makeDate with an import/use of that shared function; update
FuzzTestRunner.re to call DateHelpers.makeDate instead of its own makeDate and
remove the duplicate definition, and update any other modules using the
duplicated function to reference the single shared symbol (e.g.,
DateHelpers.makeDate) so only one implementation remains.
example/shared/Scenario.re (1)

84-97: Consider extracting the CLI argument parsing pattern.

This current() function follows the same pattern as RangeScenario.current(): read last CLI arg, validate against an allow-list, fallback to default. Consider extracting a shared helper like:

let currentScenarioName = (~all, ~default) => ...

This would reduce duplication and ensure consistent CLI handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/shared/Scenario.re` around lines 84 - 97, The current() function
duplicates the CLI-arg parsing found in RangeScenario.current(); extract that
logic into a shared helper (e.g., currentScenarioName with signature like (~all,
~default) => string) then have current() and RangeScenario.current() call this
helper and pass their allow-list and default; locate and replace the duplicated
conditional that reads Sys.argv, picks the last arg, validates with Array.exists
against all, and returns either the candidate or "default", and ensure you
continue to pass the resulting name into byName(name) in current() and the
equivalent call in RangeScenario.current().
example/fuzz/FuzzGenerator.re (1)

133-148: Date rollover may produce unexpected month boundary crossings.

When fromDay is late in the month (e.g., day 28), adding up to 14 days (toDay = fromDay +. 14.0 = 42) will cause Js.Date to roll over into the next month. This may be acceptable for fuzz testing, but could produce ranges that span months unexpectedly.

If you want to constrain rangeTo to the same month, clamp toDay to 28 (or calculate based on actual month length).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/fuzz/FuzzGenerator.re` around lines 133 - 148, The current rangeTo
calculation (involving rangeTo, rangeFrom, fromDay, toDay, randomInt,
Js.Date.make) can roll into the next month; to fix, compute the last day of the
month for Js.Date.getMonth(from)/Js.Date.getFullYear(from) (e.g., by
constructing the first day of the next month and subtracting one day or using
the standard JS date trick) and clamp toDay to Math.min(toDay, lastDayOfMonth)
before calling Js.Date.make so the generated range stays within the same month
(alternatively, clamp to 28 if you intentionally want a fixed safe cap).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example/fuzz/FuzzGenerator.re`:
- Around line 174-181: generateConfig currently uses Random.int(3) before
seeding the PRNG, making mode selection non-deterministic; seed the PRNG by
calling Random.init(seed) at the start of generateConfig (before Random.int(3))
so the selection is derived from the provided seed, and ensure the downstream
generators (generateSingleConfig, generateMultipleConfig, generateRangeConfig)
either rely on this seeded state or do not re-init the RNG in a conflicting way.

In `@example/fuzz/FuzzTestRunner.re`:
- Around line 19-23: The testFuzzConfig helper uses a tautological assertion
(Alcotest.check(Alcotest.bool) ... true, true) which never validates the
runNativeTest result; change testFuzzConfig to capture the value returned by
runNativeTest and assert on its contents instead (for example, call let output =
runNativeTest(config) and then use Alcotest.check(Alcotest.string) or
Alcotest.(check (list string)) to verify expected HTML substrings, DOM markers,
or compare against a stored snapshot); update references in testFuzzConfig and
any callers so assertions verify meaningful properties of output rather than
always-true literals.

In `@example/README.md`:
- Line 13: Update the skipped heading levels by changing the "### Run the native
renderer" heading to an h2 (## Run the native renderer) and likewise convert the
other top-level section headings currently using "###" (the headings at the
other section titles) to "##" so the document flows from the existing h1 to h2
before any h3s; ensure subsequent subheadings that are intended to be nested
remain at h3.

---

Nitpick comments:
In `@example/fuzz/FuzzGenerator.re`:
- Around line 133-148: The current rangeTo calculation (involving rangeTo,
rangeFrom, fromDay, toDay, randomInt, Js.Date.make) can roll into the next
month; to fix, compute the last day of the month for
Js.Date.getMonth(from)/Js.Date.getFullYear(from) (e.g., by constructing the
first day of the next month and subtracting one day or using the standard JS
date trick) and clamp toDay to Math.min(toDay, lastDayOfMonth) before calling
Js.Date.make so the generated range stays within the same month (alternatively,
clamp to 28 if you intentionally want a fixed safe cap).

In `@example/fuzz/FuzzRenderer.re`:
- Around line 54-68: The "range" branch in FuzzRenderer.re currently returns
Js.Undefined.fromOption(None) when only one bound is present, which prevents
creating partial ranges; update the logic to mirror RangeRenderer.re by
constructing a range object even when only from or only to is present: when
config.rangeFrom is Some(from) and config.rangeTo is None, return a Range where
ReactDayPicker.from is Js.Undefined.return(from) and ReactDayPicker.to_ is
Js.Undefined.fromOption(None); likewise when rangeTo is Some(to_) and rangeFrom
is None construct a Range with ReactDayPicker.to_ set and ReactDayPicker.from as
Js.Undefined.fromOption(None); keep the existing full-range case unchanged so
partial selections are testable by the fuzzer.
- Around line 19-31: The switch blocks in renderFuzzConfig (for captionLayout
and navLayout) are identity mappings; replace the switch logic by directly using
the existing option values: assign captionLayout = config.captionLayout and
navLayout = config.navLayout (i.e., remove the redundant switch over
config.captionLayout and config.navLayout and use the fields as-is) so the
function is simpler and equivalent.

In `@example/fuzz/FuzzTestRunner.re`:
- Around line 25-34: The makeDate implementation in FuzzTestRunner.re duplicates
RangeScenario.makeDate; extract the shared implementation into a common utility
(e.g., DateHelpers.makeDate) and replace the local makeDate with an import/use
of that shared function; update FuzzTestRunner.re to call DateHelpers.makeDate
instead of its own makeDate and remove the duplicate definition, and update any
other modules using the duplicated function to reference the single shared
symbol (e.g., DateHelpers.makeDate) so only one implementation remains.

In `@example/shared/ExampleDayPickers.re`:
- Around line 3-12: The function makeDate is defined but unused; either remove
it or replace the inline Js.Date.make call used for closeDate with the helper.
Locate makeDate and the closeDate construction in ExampleDayPickers.re, then
either delete the makeDate declaration or refactor closeDate to call
makeDate(~year, ~month, ~day) (keep the existing hours/minutes/seconds behavior
by ensuring makeDate produces noon) so there’s no unused helper left behind.

In `@example/shared/RangeScenario.re`:
- Around line 8-17: The helper function makeDate is duplicated and should be
extracted into a shared utility module; create a new module (e.g., DateUtils.re)
that exports makeDate with the same signature (let makeDate = (~year:int,
~month:int, ~day:int): Js.Date.t => ...), replace the inline implementations in
RangeScenario.re and FuzzTestRunner.re with imports/uses of DateUtils.makeDate,
and update any module references or build entries so both modules use the single
shared implementation.
- Around line 19-21: The constant `nextWeek` is declared via
`makeDate(~year=2026, ~month=2, ~day=28)` but never used; either remove the
`nextWeek` declaration to eliminate dead code or integrate `nextWeek` into your
scenario definitions (e.g., a week-long range scenario) where `today` and
`tomorrow` are used; update any scenario functions that should reference a week
range to use `nextWeek` (or delete the `nextWeek` binding if no such scenario is
needed).
- Around line 30-40: The public function byName currently silently maps unknown
names to (Some(today), Some(tomorrow)) via the wildcard branch; update byName to
make this behavior explicit by either (A) replacing the wildcard branch with an
explicit failure (e.g., raise/invalid argument including the unknown name) so
callers get immediate feedback, or (B) keep the fallback but add a clear
docstring/comment on byName describing that unknown names will map to
(Some(today), Some(tomorrow)) and that current()/all validation is expected
elsewhere; reference the byName function and the today/tomorrow identifiers when
making the change.

In `@example/shared/Scenario.re`:
- Around line 84-97: The current() function duplicates the CLI-arg parsing found
in RangeScenario.current(); extract that logic into a shared helper (e.g.,
currentScenarioName with signature like (~all, ~default) => string) then have
current() and RangeScenario.current() call this helper and pass their allow-list
and default; locate and replace the duplicated conditional that reads Sys.argv,
picks the last arg, validates with Array.exists against all, and returns either
the candidate or "default", and ensure you continue to pass the resulting name
into byName(name) in current() and the equivalent call in
RangeScenario.current().

In `@ReactDayPicker.re`:
- Around line 93-106: The Obj.magic conversions in toJsSelected and toJsOnSelect
(and the use of toJsRangeDate/fromJsRangeDate) are unsafe for JS interop; add
concise inline comments above toJsSelected and toJsOnSelect documenting the
exact JS shapes and callback signatures expected by react-day-picker for
`Single`, `Multiple`, and `Range` (e.g., Single -> Date, Multiple ->
Array<Date>, Range -> { from?: Date, to?: Date }, and the Range callback
receives that shape), and note that Obj.magic is used intentionally for brevity
so future API changes are easier to spot; keep the comments next to the function
definitions for quick discovery.

In `@ReactDayPickerNative.re`:
- Around line 332-337: The getWeekNumber function currently computes weeks by
dividing days since year start using dateYear, newDate, dayOfWeek and
getDayOfYear, which does not follow ISO 8601 (week 1 contains the year's first
Thursday); to fix, replace or augment getWeekNumber to implement ISO rules:
determine the Thursday-based week anchor (adjust the date by (4 - dayOfWeek)
days), compute the year of that anchor, then compute week number as 1 +
floor((dayOfYear(anchor) - 1) / 7); update getWeekNumber to use this algorithm
(keeping helpers dateYear, newDate, dayOfWeek, getDayOfYear) so week numbers
match ISO 8601 when required.

In `@reason-react-day-picker.opam.template`:
- Around line 1-3: The pin-depends entry currently references
"server-reason-react.dev" at repository HEAD; change the pin-depends value to
reference a specific commit hash or release tag (e.g., replace the git+https URL
with git+https://...#<commit-hash-or-tag>) to ensure reproducible builds, and if
leaving it unpinned during active development is intentional, add a short
comment in the template noting it must be pinned to a release tag before
publishing; update the pin-depends array and the "server-reason-react.dev" entry
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 164c80de-768f-4775-9bb3-3451de12778c

📥 Commits

Reviewing files that changed from the base of the PR and between 18e62fc and 85d5120.

📒 Files selected for processing (28)
  • .gitignore
  • README.md
  • ReactDayPicker.re
  • ReactDayPickerNative.re
  • dune
  • dune-project
  • example/README.md
  • example/check-parity.sh
  • example/check-range-parity.sh
  • example/fuzz-test.sh
  • example/fuzz/FuzzGenerator.re
  • example/fuzz/FuzzRenderer.re
  • example/fuzz/FuzzTestRunner.re
  • example/fuzz/dune
  • example/js/JsRenderer.re
  • example/js/RangeJsRenderer.re
  • example/js/dune
  • example/native/NativeRenderer.re
  • example/native/RangeNativeRenderer.re
  • example/native/dune
  • example/shared/ExampleDayPickers.re
  • example/shared/RangeRenderer.re
  • example/shared/RangeScenario.re
  • example/shared/Scenario.re
  • example/shared/SharedFixture.re
  • native/dune
  • reason-react-day-picker.opam
  • reason-react-day-picker.opam.template

Comment on lines +174 to +181
let generateConfig = (~seed, ~baseYear, ~baseMonth): fuzzConfig => {
let modeType = Random.int(3);
switch (modeType) {
| 0 => generateSingleConfig(~seed, ~baseYear, ~baseMonth)
| 1 => generateMultipleConfig(~seed, ~baseYear, ~baseMonth)
| _ => generateRangeConfig(~seed, ~baseYear, ~baseMonth)
};
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: Mode selection uses uninitialized PRNG state.

generateConfig calls Random.int(3) before any Random.init(seed) call. The mode selection is therefore based on whatever previous PRNG state exists, not the provided seed. This makes the mode selection non-deterministic across runs.

🐛 Proposed fix
 let generateConfig = (~seed, ~baseYear, ~baseMonth): fuzzConfig => {
+  Random.init(seed);
   let modeType = Random.int(3);
   switch (modeType) {
   | 0 => generateSingleConfig(~seed, ~baseYear, ~baseMonth)
   | 1 => generateMultipleConfig(~seed, ~baseYear, ~baseMonth)
   | _ => generateRangeConfig(~seed, ~baseYear, ~baseMonth)
   };
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let generateConfig = (~seed, ~baseYear, ~baseMonth): fuzzConfig => {
let modeType = Random.int(3);
switch (modeType) {
| 0 => generateSingleConfig(~seed, ~baseYear, ~baseMonth)
| 1 => generateMultipleConfig(~seed, ~baseYear, ~baseMonth)
| _ => generateRangeConfig(~seed, ~baseYear, ~baseMonth)
};
};
let generateConfig = (~seed, ~baseYear, ~baseMonth): fuzzConfig => {
Random.init(seed);
let modeType = Random.int(3);
switch (modeType) {
| 0 => generateSingleConfig(~seed, ~baseYear, ~baseMonth)
| 1 => generateMultipleConfig(~seed, ~baseYear, ~baseMonth)
| _ => generateRangeConfig(~seed, ~baseYear, ~baseMonth)
};
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/fuzz/FuzzGenerator.re` around lines 174 - 181, generateConfig
currently uses Random.int(3) before seeding the PRNG, making mode selection
non-deterministic; seed the PRNG by calling Random.init(seed) at the start of
generateConfig (before Random.int(3)) so the selection is derived from the
provided seed, and ensure the downstream generators (generateSingleConfig,
generateMultipleConfig, generateRangeConfig) either rely on this seeded state or
do not re-init the RNG in a conflicting way.

Comment on lines +19 to +23
let testFuzzConfig = (name, config) =>
Alcotest.test_case(name, `Quick, () => {
let _output = runNativeTest(config);
Alcotest.check(Alcotest.bool)("Native renders successfully", true, true);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test assertion is a tautology and provides no real verification.

The check Alcotest.check(Alcotest.bool)("Native renders successfully", true, true) always passes. This only confirms that runNativeTest didn't throw an exception, but doesn't validate the rendered output content.

Consider adding meaningful assertions, such as verifying the output contains expected HTML structure or comparing against a snapshot.

💡 Suggested improvement
 let testFuzzConfig = (name, config) =>
   Alcotest.test_case(name, `Quick, () => {
     let _output = runNativeTest(config);
-    Alcotest.check(Alcotest.bool)("Native renders successfully", true, true);
+    /* At minimum, verify output is non-empty */
+    Alcotest.check(Alcotest.bool)("Native renders non-empty output", true, String.length(_output) > 0);
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let testFuzzConfig = (name, config) =>
Alcotest.test_case(name, `Quick, () => {
let _output = runNativeTest(config);
Alcotest.check(Alcotest.bool)("Native renders successfully", true, true);
});
let testFuzzConfig = (name, config) =>
Alcotest.test_case(name, `Quick, () => {
let _output = runNativeTest(config);
/* At minimum, verify output is non-empty */
Alcotest.check(Alcotest.bool)("Native renders non-empty output", true, String.length(_output) > 0);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/fuzz/FuzzTestRunner.re` around lines 19 - 23, The testFuzzConfig
helper uses a tautological assertion (Alcotest.check(Alcotest.bool) ... true,
true) which never validates the runNativeTest result; change testFuzzConfig to
capture the value returned by runNativeTest and assert on its contents instead
(for example, call let output = runNativeTest(config) and then use
Alcotest.check(Alcotest.string) or Alcotest.(check (list string)) to verify
expected HTML substrings, DOM markers, or compare against a stored snapshot);
update references in testFuzzConfig and any callers so assertions verify
meaningful properties of output rather than always-true literals.

single picker and a range picker for the same fixture values in
`example/shared/SharedFixture.re`, including `footer` via `React.string(...)`.

### Run the native renderer
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Heading levels skip from h1 to h3.

The document jumps directly from # (h1) to ### (h3) without an intermediate ## (h2). This breaks the document outline hierarchy and can affect screen reader navigation.

Consider using ## for these section headings, or adding an intermediate h2 section header.

📝 Suggested fix
-### Run the native renderer
+## Run the native renderer

Apply similar changes to other ### headings (lines 19, 30, 42, 49).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Run the native renderer
## Run the native renderer
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/README.md` at line 13, Update the skipped heading levels by changing
the "### Run the native renderer" heading to an h2 (## Run the native renderer)
and likewise convert the other top-level section headings currently using "###"
(the headings at the other section titles) to "##" so the document flows from
the existing h1 to h2 before any h3s; ensure subsequent subheadings that are
intended to be nested remain at h3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant